/*    

 bobbietalk - a visual programming environment for toy robotics

 Copyright (C) 2013  Roberto Tiella

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

 */
package bobbietalk.gui;

import bobbietalk.Utilities;
import bobbietalk.gui.undo.UnduableActionEvent;
import bobbietalk.gui.undo.UnduableActionsListener;
import bobbietalk.gui.undo.UnduableAction;
import bobbietalk.gui.undo.BlockDeleteUnduableAction;
import bobbietalk.gui.blocks.ForwardBlock;
import bobbietalk.gui.blocks.EndBlock;
import bobbietalk.gui.blocks.BeginBlock;
import bobbietalk.gui.blocks.ConditionBlock;
import bobbietalk.gui.blocks.ifblk.open.IfOpenBlock;
import bobbietalk.gui.blocks.ifblk.open.IfOpenFalseBlock;
import bobbietalk.gui.blocks.ifblk.open.IfOpenTrueBlock;
import bobbietalk.gui.blocks.NumberBlock;
import bobbietalk.gui.blocks.OneParameterBlock;
import bobbietalk.gui.blocks.ProgramLoader;
import bobbietalk.gui.blocks.parametrized.ParametrizedBlock;
import bobbietalk.gui.blocks.ifblk.close.IfCloseBlock;
import bobbietalk.gui.blocks.ifblk.close.IfCloseFalseBlock;
import bobbietalk.gui.blocks.ifblk.close.IfCloseTrueBlock;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author roberto
 */
public class ProgrammingBoard extends javax.swing.JPanel {

    private int initX = 100;
    private int initY = 100;
    private ProgrammingBlock selected;
    // private int id = 1;
    private boolean programChanged;
    private Queue<UnduableAction> unduableActions = new ArrayDeque<>();
    private List<UnduableActionsListener> unduableActionsListeners = new ArrayList<>();
    private List<SelectionChangedListener> selectionChangedListeners = new ArrayList<>();
    private MainForm mainFrame;
    private boolean gridEnabled;
    private List<ProgramChangedListener> programChangedListeners = new ArrayList<>();

    ;

    /**
     * Creates new form ProgrammingBoard
     */
    public ProgrammingBoard() {

        initComponents();

        addMouseListener(new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {

                if (selected != null) {

                    selected.setSelected(false);
                    selected = null;

                    fireSelectionChanged();

                }

            }

            @Override
            public void mousePressed(MouseEvent e) {
//                throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
            }

            @Override
            public void mouseReleased(MouseEvent e) {
//                throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
            }

            @Override
            public void mouseEntered(MouseEvent e) {
//                throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
            }

            @Override
            public void mouseExited(MouseEvent e) {
//                throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
            }
        });

    }

    public void setMainFrame(MainForm mainFrame) {
        this.mainFrame = mainFrame;
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 400, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 300, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
    public void addBlock(String type) throws BobbietalkException {

        unduableActions.clear();
        fireUnduableActionsChanged();

        ProgrammingBlock b1 = null;

        if (type.equals("begin")) {

            b1 = new BeginBlock();


        } else if (type.equals("end")) {

            b1 = new EndBlock();


        } else if (type.equals("number")) {

            b1 = new NumberBlock();


        } else if (type.equals("condition")) {

            b1 = new ConditionBlock();

        } else if (type.equals("ifopen")) {

            IfOpenBlock b = new IfOpenBlock();
            b.setBoard(this);

            final IfOpenTrueBlock ifTrueBlock = new IfOpenTrueBlock();
            b.setIfTrueBlock(ifTrueBlock);
            ifTrueBlock.setBoard(this);
            this.add(ifTrueBlock);


            final IfOpenFalseBlock ifFalseBlock = new IfOpenFalseBlock();
            b.setIfFalseBlock(ifFalseBlock);
            ifFalseBlock.setBoard(this);
            this.add(ifFalseBlock);

            b1 = b;

        } else if (type.equals("ifclose")) {

            IfCloseBlock b = new IfCloseBlock();

            final IfCloseTrueBlock ifTrueBlock = new IfCloseTrueBlock();
            b.setIfTrueBlock(ifTrueBlock);
            this.add(ifTrueBlock);

            final IfCloseFalseBlock ifFalseBlock = new IfCloseFalseBlock();
            b.setIfFalseBlock(ifFalseBlock);
            this.add(ifFalseBlock);

            b1 = b;

        } else {
            throw new BobbietalkException("can't create a block of type:" + type);
        }

        b1.setBoard(this);
        initGraphicFeatures(b1);

        b1.setName(String.format("%s-%d", b1.getType(), ProgramLoader.nextId()));


        b1.setPosition(initX, initY);
        initX += 10;
        initY += 10;

        this.add(b1);

        programChanged = true;
        fireProgramChangedEvent();

        this.repaint();
    }

    public void dumpContent(PrintWriter pw) {

        pw.println("BEGIN:");

        for (int i = 0; i < getComponentCount(); i++) {

            if (getComponent(i) instanceof ProgrammingBlock) {

                ProgrammingBlock b = (ProgrammingBlock) getComponent(i);

                b.dump(pw);

                pw.println();

            }

        }

        pw.println("END.");

    }

    public void dumpProgram() {

        ProgrammingBlock first = findFirst();

        if (first == null) {

            System.out.println("no program found!");
            return;

        }

        while (first != null) {


            System.out.println(first.getText());
            first = first.getNextBlock();

        }

    }

    void deleteSelectedBlock() {

        if (selected != null) {

            String className = selected.getClass().getSimpleName();
            String name = selected.getName();
            String nextName = getNameOrNull(selected.getNextBlock());
            String prevName = getNameOrNull(selected.getPreviousBlock());

            BlockDeleteUnduableAction act =
                    new BlockDeleteUnduableAction(this, className, name, nextName,
                    prevName, selected.getX(), selected.getY());

            unduableActions.add(act);
            fireUnduableActionsChanged();

            selected.toBedeleted();
            remove(selected);

            selected = null;
            programChanged = true;

            fireSelectionChanged();
            fireProgramChangedEvent();

            repaint();

        }


    }

    void setSelected(ProgrammingBlock block) {

        if (selected != null) {
            selected.setSelected(false);
        }

        selected = block;
        fireSelectionChanged();

        repaint();

    }

    public String serializeProgram() {

        List<ProgrammingBlock> blocks = getBlocks();

        ProgramCollector pc = new ProgramCollector();
        pc.collect(blocks);

        return pc.getProgram();
    }

    void saveProgram(File selectedFile) {

        try {

            List<ProgrammingBlock> blocks = getBlocks();

            ProgramCollector pc = new ProgramCollector();
            pc.collect(blocks);

            PrintWriter pw = new PrintWriter(selectedFile);

            pw.printf(pc.getProgram());

            pw.close();

            programChanged = false;
            unduableActions.clear();
            fireUnduableActionsChanged();

        } catch (FileNotFoundException ex) {
            Utilities.getLogger().log(Level.SEVERE, null, ex);
        }



    }

    private ProgrammingBlock findFirst() {

        ProgrammingBlock first = null;
        for (int i = 0; first == null && i < getComponentCount(); i++) {

            if (getComponent(i) instanceof BeginBlock) {

                first = (ProgrammingBlock) getComponent(i);

            }

        }
        return first;
    }

    public void loadProgram(File selectedFile) throws FileNotFoundException {

        loadProgram(new BufferedReader(new FileReader(selectedFile)));

    }

    public void loadProgram(Reader reader) {
        try {


            for (ProgrammingBlock b : getBlocks()) {
                remove(b);
            }

            ArrayList<ProgrammingBlock> program = new ArrayList<>();

            ProgramLoader pl = new ProgramLoader(mainFrame.getBlockFactory());

            pl.sourceBlocks(reader, program);

            for (ProgrammingBlock b : program) {

                b.setBoard(this);
                initGraphicFeatures(b);
                this.add(b);

            }

            repaint();

            unduableActions.clear();
            fireUnduableActionsChanged();
            programChanged = false;


        } catch (FileNotFoundException ex) {
            Utilities.getLogger().log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Utilities.getLogger().log(Level.SEVERE, null, ex);
        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            Utilities.getLogger().log(Level.SEVERE, null, ex);
        }


    }

    public boolean isProgramChanged() {
        return programChanged;
    }

    public void undo() {

        UnduableAction remove = unduableActions.remove();
        remove.redo();
        fireUnduableActionsChanged();

    }

    public void createAndAddBlock(String blockClass, String blockName, String nextReference, String previousReference, int x, int y) throws NoSuchMethodException, InstantiationException, SecurityException, ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {

        ProgrammingBlock b = ProgramLoader.createBlock(blockClass);


        ProgramLoader.assignBlockName(b, blockName);

        if (!nextReference.equals("null")) {
            ProgrammingBlock ref = getComponentByName(nextReference);
            b.setNextBlock(ref);
            ref.setPreviousBlock(b);
        }

        if (!previousReference.equals("null")) {
            ProgrammingBlock ref = getComponentByName(previousReference);
            b.setPreviousBlock(ref);
            ref.setNextBlock(b);
        }

        ProgramLoader.assignBlockX(b, Integer.toString(x));
        ProgramLoader.assignBlockY(b, Integer.toString(y));

        b.setBoard(this);
        initGraphicFeatures(b);
        this.add(b);

        repaint();

    }

    private ProgrammingBlock getComponentByName(String nextReference) {

        for (int i = 0; i < getComponentCount(); i++) {

            Component component = getComponent(i);

            if (component instanceof ProgrammingBlock) {

                if (component.getName().equals(nextReference)) {

                    return (ProgrammingBlock) component;

                }

            }

        }

        return null;

    }

    private String getNameOrNull(ProgrammingBlock b) {

        if (b != null) {
            return b.getName();
        } else {
            return "null";
        }

    }

    public void changed(ProgrammingBlock aThis) {
        unduableActions.clear();
        fireUnduableActionsChanged();
    }

    private void fireUnduableActionsChanged() {

        UnduableActionEvent ev = new UnduableActionEvent();

        if (unduableActions.isEmpty()) {
            ev.setType(UnduableActionEvent.Type.QUEUE_EMPTY);
        } else {
            ev.setType(UnduableActionEvent.Type.QUEUE_NOT_EMPTY);
        }

        for (UnduableActionsListener l : unduableActionsListeners) {
            l.handleUnduableActionEvent(ev);
        }

    }

    public void addUnduableActionsListener(UnduableActionsListener e) {
        unduableActionsListeners.add(e);
    }

    public void removeUnduableActionsListener(UnduableActionsListener o) {
        unduableActionsListeners.remove(o);
    }

    public void fireSelectionChanged() {

        SelectionChangedEvent ev = new SelectionChangedEvent();

        if (selected == null) {

            ev.setSelectionSize(0);

        } else {
            ev.setSelectionSize(1);
        }


        for (SelectionChangedListener l : selectionChangedListeners) {
            l.handleSelectionChangedEvent(ev);
        }

    }

    public void addSelectionChangedListener(SelectionChangedListener e) {
        selectionChangedListeners.add(e);
    }

    public void remove(SelectionChangedListener o) {
        selectionChangedListeners.remove(o);
    }

    List<ProgrammingBlock> getBlocks() {

        ArrayList<ProgrammingBlock> blocks = new ArrayList<>();

        for (int i = 0; i < getComponentCount(); i++) {

            if (getComponent(i) instanceof ProgrammingBlock) {

                blocks.add((ProgrammingBlock) getComponent(i));

            }

        }

        return blocks;
    }

    private void initGraphicFeatures(ProgrammingBlock b1) {

        b1.initListeners();

        if (b1.getIcon() != null) {
            b1.setIconOffsetY(b1.getHeight() / 2 - b1.getIcon().getIconHeight() * 2 / 3);
        }

        if (b1 instanceof OneParameterBlock) {

            b1.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {

                    OneParameterBlock b = (OneParameterBlock) e.getComponent();

                    if (e.getClickCount() == 2) {

                        int[] n = new int[1];

                        n[0] = b.getValue();

                        if (mainFrame.openNumberDialog(n)) {

                            b.setValue(n[0]);
                            b.update();

                            //  if user had changed the values the program is updated
                            programChanged = true;
                            fireProgramChangedEvent();

                        }

                    }

                }
            });

        }

        if (b1 instanceof ParametrizedBlock) {

            b1.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {

                    ParametrizedBlock b = (ParametrizedBlock) e.getComponent();

                    if (e.getClickCount() == 2) {

                        mainFrame.openBlockEditingDialog(b);

                        // TODO if user had changed the values the program is updated
                        //  programChanged = true;
                        // fireProgramChangedEvent();

                    }

                }
            });

        }
    }

    public boolean isProgramEmpty() {

        return getBlocks().isEmpty();

    }

    public void clearProgram() {

        for (ProgrammingBlock b : getBlocks()) {

            remove(b);

        }

        fireProgramChangedEvent();
        repaint();

    }

    @Override
    protected void paintComponent(Graphics g) {



        // TODO scaling works but then objects cannot be dragged properly g2d.scale(0.5, 0.5);

        super.paintComponent(g);

        //To change body of generated methods, choose Tools | Templates.

        if (gridEnabled) {

            Graphics2D g2d = (Graphics2D) g;

            g2d.setColor(Color.LIGHT_GRAY);

            Rectangle bs = getBounds();

            for (int x = 0; x < bs.width; x += 10) {

                g2d.drawLine(x, 0, x, bs.height);

            }

            for (int y = 0; y < bs.height; y += 10) {

                g2d.drawLine(0, y, bs.width, y);

            }

        }
    }

    public boolean isGridEnabled() {
        return gridEnabled;
    }

    public void setGridEnabled(boolean gridEnabled) {
        this.gridEnabled = gridEnabled;
    }

    public void addGeneric(ProgrammingBlock b) {

        b.setBoard(this);

        initGraphicFeatures(b);

        b.setName(String.format("%s-%d", b.getType(), ProgramLoader.nextId()));

        b.setPosition(initX, initY);
        initX += 10;
        initY += 10;

        this.add(b);

        programChanged = true;
        fireProgramChangedEvent();

        this.repaint();


    }

    public void addProgramChangedListener(ProgramChangedListener l) {

        programChangedListeners.add(l);

    }

    public void fireProgramChangedEvent() {

        ProgramChangedEvent ev = new ProgramChangedEvent();

        for (ProgramChangedListener l : programChangedListeners) {

            l.handleProgramChangedEvent(ev);

        }

    }

    public void setProgramChanged(boolean b) {
        programChanged = b;
    }

    void setBlocks(List<ProgrammingBlock> blocks) {

        for (ProgrammingBlock b : blocks) {

            add(b);

        }

        repaint();

    }

    public ProgrammingBlock getSelectedBlock() {
        
        return selected;
        
    }
}
